<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="k9jwe" id="k9jwe"><span data-lake-id="ub7677a52" id="ub7677a52">典型回答</span></h1>
  <p data-lake-id="uc6136736" id="uc6136736"><br></p>
  <p data-lake-id="u9926daf6" id="u9926daf6"><span data-lake-id="ud02bc1c5" id="ud02bc1c5">为了保证Redis和数据库的数据一致性，肯定是要缓存和数据库双写了。</span></p>
  <p data-lake-id="u3491640f" id="u3491640f"><span data-lake-id="uf66ab2ec" id="uf66ab2ec">​</span><br></p>
  <p data-lake-id="u63b6a3c7" id="u63b6a3c7"><span data-lake-id="u05116ce3" id="u05116ce3">这时候就需要考虑两个问题：</span><strong><span data-lake-id="u12f0dfb2" id="u12f0dfb2">是先操作缓存还是先操作数据库？是删除缓存还是更新缓存？</span></strong></p>
  <p data-lake-id="uf6af4181" id="uf6af4181"><br></p>
  <p data-lake-id="u12db4341" id="u12db4341"><span data-lake-id="u5d8c35e5" id="u5d8c35e5">我的建议是</span><strong><span data-lake-id="u96d3823b" id="u96d3823b">优先考虑删除缓存而不是更新缓存，</span></strong><span data-lake-id="ub283fc86" id="ub283fc86">因为删除缓存更加简单，而且带来的一致性问题也更少一些。</span></p>
  <p data-lake-id="ud68a8537" id="ud68a8537"><span data-lake-id="u94100f76" id="u94100f76">​</span><br></p>
  <p data-lake-id="u5f9d95f6" id="u5f9d95f6"><span data-lake-id="u17e3f70a" id="u17e3f70a">另外，在具体操作过程中，建议考虑</span><strong><span data-lake-id="u7ca97199" id="u7ca97199">延迟双删的策略</span></strong><span data-lake-id="u8171a12e" id="u8171a12e">，即：</span></p>
  <p data-lake-id="u31618acf" id="u31618acf"><span data-lake-id="u14668984" id="u14668984">​</span><br></p>
  <ul list="ud218826d">
   <li fid="u51abb5a6" data-lake-id="u78be8f48" id="u78be8f48"><span data-lake-id="u81195c4a" id="u81195c4a">Step 1 , 删除缓存</span></li>
   <li fid="u51abb5a6" data-lake-id="u364b4c52" id="u364b4c52"><span data-lake-id="u9f670f4d" id="u9f670f4d">Step 2, 更新数据库</span></li>
   <li fid="u51abb5a6" data-lake-id="uba7efc2e" id="uba7efc2e"><span data-lake-id="u1b84b12a" id="u1b84b12a">Step 3, 再次删除缓存</span></li>
  </ul>
  <p data-lake-id="u1f6666ae" id="u1f6666ae"><span data-lake-id="u6d6cbd3a" id="u6d6cbd3a">​</span><br></p>
  <p data-lake-id="ud0c49e6c" id="ud0c49e6c"><span data-lake-id="uaaa0cbee" id="uaaa0cbee">也就是说在先删除缓存，再更新数据库，然后过个几秒再删一把缓存，避免因为并发出现脏数据。</span></p>
  <p data-lake-id="ubc36e9b3" id="ubc36e9b3"><span data-lake-id="u2197de17" id="u2197de17">​</span><br></p>
  <p data-lake-id="ua6d912e7" id="ua6d912e7"><span data-lake-id="u743c749c" id="u743c749c">一般为了提升稳定性，降低对代码的侵入性，还可以考虑把缓存的删除（更新）做成异步化，通过MQ或者监听数据库binlog的方式来处理。</span></p>
  <p data-lake-id="u6366a8ee" id="u6366a8ee"><br></p>
  <h1 data-lake-id="mPUfx" id="mPUfx"><span data-lake-id="u0ad96113" id="u0ad96113">扩展知识</span></h1>
  <p data-lake-id="uda10465c" id="uda10465c"><br></p>
  <h2 data-lake-id="MYnSo" id="MYnSo"><span data-lake-id="u74689111" id="u74689111">删除而不是更新</span></h2>
  <p data-lake-id="uc0b276df" id="uc0b276df"><br></p>
  <p data-lake-id="uf0c7ede0" id="uf0c7ede0"><span data-lake-id="u81b22a28" id="u81b22a28">为了保证数据库和缓存里面的数据是一致的，很多人会在做数据更新的时候，会同时更新缓存里面的内容。</span><strong><span data-lake-id="u0e292eb4" id="u0e292eb4">但是我其实告诉大家，应该优先选择删除缓存而不是更新缓存。</span></strong></p>
  <p data-lake-id="u3dd13e64" id="u3dd13e64"><br></p>
  <p data-lake-id="u3e6d3910" id="u3e6d3910"><span data-lake-id="u2c151824" id="u2c151824">首先，我们暂时抛开数据一致性的问题，单独来看看更新缓存和删除缓存的复杂的问题。</span></p>
  <p data-lake-id="u2b348319" id="u2b348319"><br></p>
  <p data-lake-id="u22e0fd72" id="u22e0fd72"><span data-lake-id="udd4075a2" id="udd4075a2">我们放到缓存中的数据，很多时候可能不只是简单的一个字符串类型的值，他还可能是一个大的JSON串，一个map类型等等。</span></p>
  <p data-lake-id="u6150bd50" id="u6150bd50"><br></p>
  <p data-lake-id="ub9841a17" id="ub9841a17"><span data-lake-id="u27b61377" id="u27b61377">举个例子，我们需要通过缓存进行扣减库存的时候，你可能需要从缓存中查出整个订单模型数据，把他进行反序列化之后，再解析出其中的库存字段，把他修改掉，然后再序列化，最后再更新到缓存中。</span></p>
  <p data-lake-id="u4db07938" id="u4db07938"><br></p>
  <p data-lake-id="ubdb378d3" id="ubdb378d3"><span data-lake-id="u192a724c" id="u192a724c">可以看到，</span><strong><span data-lake-id="u2ed518c0" id="u2ed518c0">更新缓存的动作，相比于直接删除缓存，操作过程比较的复杂，而且也容易出错。</span></strong></p>
  <p data-lake-id="u413ad953" id="u413ad953"><br></p>
  <p data-lake-id="ub4af5da1" id="ub4af5da1"><span data-lake-id="uddb094b9" id="uddb094b9">还有就是，在数据库和缓存的一致性保证方面，删除缓存相比更新缓存要更简单一点。</span></p>
  <p data-lake-id="uded7b6ab" id="uded7b6ab"><br></p>
  <p data-lake-id="ubc1f92e1" id="ubc1f92e1"><span data-lake-id="u32250574" id="u32250574">在"写写并发"的场景中，如果同时更新缓存和数据库，那么很容易会出现因为并发的问题导致数据不一致的情况。如：</span></p>
  <p data-lake-id="u84f46513" id="u84f46513"><br></p>
  <p data-lake-id="uf21615bc" id="uf21615bc"><span data-lake-id="u405b05bb" id="u405b05bb">先写数据库，再更新缓存</span></p>
  <table data-lake-id="FJQz5" id="FJQz5" margin="true" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="375">
    <col width="375">
   </colgroup>
   <tbody>
    <tr data-lake-id="u8f75a33b" id="u8f75a33b">
     <td data-lake-id="ua824d058" id="ua824d058" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ud40c2119" id="ud40c2119" style="text-align: center"><span data-lake-id="u4fb996b3" id="u4fb996b3" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">W</span></p></td>
     <td data-lake-id="ub57c9dda" id="ub57c9dda" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ud22f79e5" id="ud22f79e5" style="text-align: center"><span data-lake-id="u3faee5f2" id="u3faee5f2" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">W</span></p></td>
    </tr>
    <tr data-lake-id="u0a55abd9" id="u0a55abd9">
     <td data-lake-id="u6c861013" id="u6c861013">
      <p data-lake-id="u6c66aa63" id="u6c66aa63" style="text-align: justify"><span data-lake-id="u9c946320" id="u9c946320" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写数据库，更新成20</span></p></td>
     <td data-lake-id="ucc1e6489" id="ucc1e6489"></td>
    </tr>
    <tr data-lake-id="u3e691c0f" id="u3e691c0f">
     <td data-lake-id="ub4ad5264" id="ub4ad5264"></td>
     <td data-lake-id="uc998b92d" id="uc998b92d">
      <p data-lake-id="u6f72944b" id="u6f72944b" style="text-align: justify"><span data-lake-id="u541e802b" id="u541e802b" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写数据库，更新成10</span></p></td>
    </tr>
    <tr data-lake-id="u55d71a98" id="u55d71a98">
     <td data-lake-id="u705adcd0" id="u705adcd0"></td>
     <td data-lake-id="u6908175e" id="u6908175e">
      <p data-lake-id="u7e1f0290" id="u7e1f0290" style="text-align: justify"><span data-lake-id="u8f309776" id="u8f309776" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写缓存，更新成10</span></p></td>
    </tr>
    <tr data-lake-id="u4e7856d3" id="u4e7856d3">
     <td data-lake-id="ubc6ba6b5" id="ubc6ba6b5">
      <p data-lake-id="u4b5784af" id="u4b5784af" style="text-align: justify"><strong><span data-lake-id="u96b0c79a" id="u96b0c79a" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写缓存，更新成20（数据不一致）</span></strong></p></td>
     <td data-lake-id="u7dfb5803" id="u7dfb5803"></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="u88a0c7b9" id="u88a0c7b9" style="text-align: justify"><span data-lake-id="ubc3f4875" id="ubc3f4875">先更新缓存，后写数据库：</span></p>
  <table data-lake-id="tUXGe" id="tUXGe" margin="true" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="375">
    <col width="375">
   </colgroup>
   <tbody>
    <tr data-lake-id="uff9b98d1" id="uff9b98d1">
     <td data-lake-id="u1d1a8461" id="u1d1a8461" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ufff89f84" id="ufff89f84" style="text-align: center"><span data-lake-id="ua096ae8f" id="ua096ae8f" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">W</span></p></td>
     <td data-lake-id="u6be0bf68" id="u6be0bf68" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ufce8a84b" id="ufce8a84b" style="text-align: center"><span data-lake-id="ufcdda7fc" id="ufcdda7fc" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">W</span></p></td>
    </tr>
    <tr data-lake-id="ua5ce8376" id="ua5ce8376">
     <td data-lake-id="u6e685dc1" id="u6e685dc1">
      <p data-lake-id="ue9afbf40" id="ue9afbf40" style="text-align: justify"><span data-lake-id="u446d59de" id="u446d59de" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写缓存，更新成20</span></p></td>
     <td data-lake-id="uc22f6871" id="uc22f6871"></td>
    </tr>
    <tr data-lake-id="u0f6199a1" id="u0f6199a1">
     <td data-lake-id="u7ff8bb67" id="u7ff8bb67"></td>
     <td data-lake-id="u24a105cd" id="u24a105cd">
      <p data-lake-id="ubacbe805" id="ubacbe805" style="text-align: justify"><span data-lake-id="u7b630dc0" id="u7b630dc0" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写缓存，更新成10</span></p></td>
    </tr>
    <tr data-lake-id="u454ee5db" id="u454ee5db">
     <td data-lake-id="ue4d09246" id="ue4d09246"></td>
     <td data-lake-id="ub419b71e" id="ub419b71e">
      <p data-lake-id="u3ebeb308" id="u3ebeb308" style="text-align: justify"><span data-lake-id="u4f3a6547" id="u4f3a6547" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写数据库，更新成10</span></p></td>
    </tr>
    <tr data-lake-id="ua9c49c50" id="ua9c49c50">
     <td data-lake-id="u549391c0" id="u549391c0">
      <p data-lake-id="u00189aa4" id="u00189aa4" style="text-align: justify"><strong><span data-lake-id="u12dca8e3" id="u12dca8e3" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写数据库，更新成20（数据不一致）</span></strong></p></td>
     <td data-lake-id="ua979cba2" id="ua979cba2"></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="ua29ecee9" id="ua29ecee9"><span data-lake-id="ud52de966" id="ud52de966">​</span><br></p>
  <p data-lake-id="u4ce2e99a" id="u4ce2e99a"><span data-lake-id="u803546da" id="u803546da">但是，</span><strong><span data-lake-id="ue622995b" id="ue622995b">如果是做缓存的删除的话，在写写并发的情况下，缓存中的数据都是要被清除的，所以就不会出现数据不一致的问题。</span></strong></p>
  <p data-lake-id="ua1f32097" id="ua1f32097"><br></p>
  <p data-lake-id="u7636a2ff" id="u7636a2ff"><span data-lake-id="u5d8a592e" id="u5d8a592e">但是，删除缓存相比更新缓存还是有一个小的缺点，那就是带来的一次额外的cache miss，也就是说在删除缓存后的下一次查询会无法命中缓存，要查询一下数据库。</span></p>
  <p data-lake-id="u9d1b53ba" id="u9d1b53ba"><br></p>
  <p data-lake-id="u7bd11099" id="u7bd11099"><strong><span data-lake-id="u5023847c" id="u5023847c">这种cache miss在某种程度上可能会导致缓存击穿</span></strong><span data-lake-id="u6c4c3578" id="u6c4c3578">，也就是刚好缓存被删除之后，同一个Key有大量的请求过来，导致缓存被击穿，大量请求访问到数据库。</span></p>
  <p data-lake-id="uf4b27ab5" id="uf4b27ab5"><br></p>
  <p data-lake-id="u257a2a78" id="u257a2a78"><span data-lake-id="u77223435" id="u77223435">但是，通过加锁的方式是可以比较方便的解决缓存击穿的问题的。</span></p>
  <p data-lake-id="u2f19554c" id="u2f19554c"><br></p>
  <p data-lake-id="ud0a83a9f" id="ud0a83a9f"><span data-lake-id="u1341faf5" id="u1341faf5">总之，删除缓存相比较更新缓存，方案更加简单，而且带来的一致性问题也更少。所以，在删除和更新缓存之间，我还是偏向于建议大家优先选择删除缓存。</span></p>
  <p data-lake-id="ua2848702" id="ua2848702"><br></p>
  <h2 data-lake-id="wBvQi" id="wBvQi"><span data-lake-id="u518b0b3b" id="u518b0b3b">先写数据库还是先删缓存</span></h2>
  <p data-lake-id="u7deaf247" id="u7deaf247"><br></p>
  <p data-lake-id="uc460d9d3" id="uc460d9d3"><span data-lake-id="ue7e6033d" id="ue7e6033d">在确定了优先选择删除缓存而不是更新缓存之后，留给我们的数据库+缓存更新的可选方案就剩下："先写数据库后删除缓存"和"先删除缓存后写数据库了"。</span></p>
  <p data-lake-id="uc15f3ddd" id="uc15f3ddd"><br></p>
  <p data-lake-id="ud61fafc0" id="ud61fafc0"><span data-lake-id="u1a65862a" id="u1a65862a">那么，这两种方式各自有什么优缺点呢？该如何选择呢？</span></p>
  <p data-lake-id="u554e1466" id="u554e1466"><br></p>
  <h3 data-lake-id="Dlqjk" id="Dlqjk"><span data-lake-id="u0a17efc7" id="u0a17efc7">先写数据库</span></h3>
  <p data-lake-id="u8ce37658" id="u8ce37658"><br></p>
  <p data-lake-id="uf01057a8" id="uf01057a8"><span data-lake-id="u006b32f0" id="u006b32f0">因为数据库和缓存的操作是两步的，没办法做到保证原子性，所以就有可能第一步成功而第二步失败。</span></p>
  <p data-lake-id="u0c5608ae" id="u0c5608ae"><br></p>
  <p data-lake-id="u2556d67c" id="u2556d67c"><span data-lake-id="u1aff3a21" id="u1aff3a21">而一般情况下，如果把缓存的删除动作放到第二步，有一个好处，那就是</span><strong><span data-lake-id="u149fb386" id="u149fb386">缓存删除失败的概率还是比较低的</span></strong><span data-lake-id="u5b69ba60" id="u5b69ba60">，除非是网络问题或者缓存服务器宕机的问题，否则大部分情况都是可以成功的。</span></p>
  <p data-lake-id="ua5ebb3d9" id="ua5ebb3d9"><span data-lake-id="ue9748f96" id="ue9748f96">​</span><br></p>
  <p data-lake-id="u719cc8a4" id="u719cc8a4"><span data-lake-id="u5a9d49ea" id="u5a9d49ea">而且，先写数据库，后删除缓存，如果第二步失败了，会导致数据库中的数据已经更新，但是缓存还是旧数据，</span><strong><span data-lake-id="u2b550d12" id="u2b550d12">导致数据不一致</span></strong><span data-lake-id="uc52a27b6" id="uc52a27b6">。</span></p>
  <p data-lake-id="u7190fa5d" id="u7190fa5d"><br></p>
  <h3 data-lake-id="f4318a96" id="f4318a96"><span data-lake-id="u65bb26e0" id="u65bb26e0">先删缓存</span></h3>
  <p data-lake-id="ud1dd1b95" id="ud1dd1b95"><br></p>
  <p data-lake-id="u67e6c7c8" id="u67e6c7c8"><span data-lake-id="u7fe7298e" id="u7fe7298e">那么，如果是先删除缓存后操作数据库的话，会不会方案更完美一点呢？</span></p>
  <p data-lake-id="ucb10077d" id="ucb10077d"><br></p>
  <p data-lake-id="u8f7ccb98" id="u8f7ccb98"><span data-lake-id="u78695ca6" id="u78695ca6">首先，</span><strong><span data-lake-id="ue3b5d4e7" id="ue3b5d4e7">如果是选择先删除缓存后写数据库的这种方案，那么第二步的失败是可以接受的</span></strong><span data-lake-id="u4d4be332" id="u4d4be332">，因为这样不会有脏数据，也没什么影响，只需要重试就好了。</span></p>
  <p data-lake-id="u7475a39f" id="u7475a39f"><br></p>
  <p data-lake-id="u37af6a86" id="u37af6a86"><span data-lake-id="ue03401d9" id="ue03401d9">但是，</span><strong><span data-lake-id="u7d7849e9" id="u7d7849e9">先删除缓存后写数据库的这种方式，会无形中放大"读写并发"导致的数据不一致的问题。</span></strong><span data-lake-id="u9d3e312e" id="u9d3e312e">我们知道，当我们使用了缓存之后，一个读的线程在查询数据的过程是这样的：</span></p>
  <p data-lake-id="u1f35d6b4" id="u1f35d6b4"><br></p>
  <p data-lake-id="u6a88a389" id="u6a88a389"><strong><span data-lake-id="u329ca169" id="u329ca169">1、查询缓存，如果缓存中有值，则直接返回 </span></strong></p>
  <p data-lake-id="u157b40e3" id="u157b40e3"><strong><span data-lake-id="ud6879c8c" id="ud6879c8c">2、查询数据库 </span></strong></p>
  <p data-lake-id="ufb16c688" id="ufb16c688"><strong><span data-lake-id="u48218f6d" id="u48218f6d">3、把数据库的查询结果更新到缓存中</span></strong></p>
  <p data-lake-id="uf1b683e3" id="uf1b683e3"><br></p>
  <p data-lake-id="u0897a2ec" id="u0897a2ec"><strong><span data-lake-id="u0410209b" id="u0410209b">所以，</span></strong><strong><span data-lake-id="u391bf7ba" id="u391bf7ba">对于一个读线程来说，虽然不会写数据库，但是是会更新缓存的</span></strong><strong><span data-lake-id="u85828e5f" id="u85828e5f">，所以，在一些特殊的并发场景中，就会导致数据不一致的情况。</span></strong></p>
  <p data-lake-id="u961dfd9d" id="u961dfd9d"><br></p>
  <p data-lake-id="ue6c96256" id="ue6c96256"><strong><span data-lake-id="uea5d1c9e" id="uea5d1c9e">读写并发的时序如下：</span></strong></p>
  <table data-lake-id="uF4yA" id="uF4yA" margin="true" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="375">
    <col width="375">
   </colgroup>
   <tbody>
    <tr data-lake-id="udff21cfb" id="udff21cfb">
     <td data-lake-id="udc708965" id="udc708965" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ub1c0b237" id="ub1c0b237" style="text-align: center"><span data-lake-id="uf591136f" id="uf591136f" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">W</span></p></td>
     <td data-lake-id="u0034c28e" id="u0034c28e" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="ua5a90585" id="ua5a90585" style="text-align: center"><span data-lake-id="u968641e2" id="u968641e2" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">R</span></p></td>
    </tr>
    <tr data-lake-id="u2d285390" id="u2d285390">
     <td data-lake-id="u342e7f63" id="u342e7f63"></td>
     <td data-lake-id="ueb2f43ce" id="ueb2f43ce">
      <p data-lake-id="u5b9f8896" id="u5b9f8896" style="text-align: justify"><span data-lake-id="uc1bafbe4" id="uc1bafbe4" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">读缓存，缓存中没有值</span></p></td>
    </tr>
    <tr data-lake-id="u76b0536b" id="u76b0536b">
     <td data-lake-id="u5a459109" id="u5a459109"></td>
     <td data-lake-id="u667a62ee" id="u667a62ee">
      <p data-lake-id="ucfa979ce" id="ucfa979ce" style="text-align: justify"><span data-lake-id="u0928abf9" id="u0928abf9" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">读数据库，数据库中得到结果为10</span></p></td>
    </tr>
    <tr data-lake-id="ud309d76c" id="ud309d76c" style="height: 37px">
     <td data-lake-id="u90c0ef6d" id="u90c0ef6d">
      <p data-lake-id="ue8a3c584" id="ue8a3c584" style="text-align: justify"><span data-lake-id="uae53fde6" id="uae53fde6" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写数据库和缓存，更新成20</span></p></td>
     <td data-lake-id="u5df265fc" id="u5df265fc"></td>
    </tr>
    <tr data-lake-id="u0fbe9efd" id="u0fbe9efd">
     <td data-lake-id="u4550105c" id="u4550105c"></td>
     <td data-lake-id="u7a85f999" id="u7a85f999">
      <p data-lake-id="u76256716" id="u76256716" style="text-align: justify"><strong><span data-lake-id="uf520648f" id="uf520648f" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">写缓存，更新成10（数据不一致）</span></strong></p></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="u11b8ba2f" id="u11b8ba2f"><strong><span data-lake-id="u214aa8b0" id="u214aa8b0">​</span></strong><br></p>
  <p data-lake-id="uff2d9ee2" id="uff2d9ee2"><strong><span data-lake-id="u524d1936" id="u524d1936">也就是说，假如一个读线程，在读缓存的时候没查到值，他就会去数据库中查询，但是如果自查询到结果之后，更新缓存之前，数据库被更新了，但是这个读线程是完全不知道的，那么就导致最终缓存会被重新用一个"旧值"覆盖掉。</span></strong></p>
  <p data-lake-id="u685d0423" id="u685d0423"><br></p>
  <p data-lake-id="u9077f0b1" id="u9077f0b1"><strong><span data-lake-id="ub4685248" id="ub4685248">这也就导致了</span></strong><strong><span data-lake-id="u568f7284" id="u568f7284">缓存和数据库的不一致的现象</span></strong><strong><span data-lake-id="u66d023fa" id="u66d023fa">。</span></strong></p>
  <p data-lake-id="udd5a9f0b" id="udd5a9f0b"><br></p>
  <p data-lake-id="u0eede4e5" id="u0eede4e5"><span data-lake-id="ufc34c0ac" id="ufc34c0ac">但是这种现象其实发生的概率比较低，因为一般一个读操作是很快的，数据库+缓存的读操作基本在十几毫秒左右就可以完成了。</span></p>
  <p data-lake-id="uddd7041a" id="uddd7041a"><br></p>
  <p data-lake-id="u44e70db9" id="u44e70db9"><strong><span data-lake-id="u74829afb" id="u74829afb">而在这期间，刚好另一个线程执行了一个比较耗时的写操作的概率确实比较低。</span></strong></p>
  <p data-lake-id="u75bd30ee" id="u75bd30ee"><br></p>
  <p data-lake-id="u3b1642d7" id="u3b1642d7"><span data-lake-id="u200cfb7e" id="u200cfb7e">因为这种"读写并发"问题发生的前提是读线程读缓存没读到值，而先删缓存的动作一旦发生，刚好可以让读线程就从缓存中读不到值。</span></p>
  <p data-lake-id="uc7043cd1" id="uc7043cd1"><br></p>
  <p data-lake-id="u1229a79e" id="u1229a79e"><span data-lake-id="u14142fc8" id="u14142fc8">所以，本来一个小概率会发生的"读写并发"问题，在先删缓存的过程中，问题发生的概率会被放大。</span></p>
  <p data-lake-id="ud7aaab35" id="ud7aaab35"><br></p>
  <p data-lake-id="u623da75b" id="u623da75b"><span data-lake-id="ua60f9580" id="ua60f9580">而且这种问题的后果也比较严重，那就是缓存中的值一直是错的，就会导致后续的所有命中缓存的查询结果都是错的！</span></p>
  <p data-lake-id="u7a8b1707" id="u7a8b1707"><br></p>
  <p data-lake-id="u7bbf2a32" id="u7bbf2a32"><span data-lake-id="ud722e6e0" id="ud722e6e0">那么怎么解决呢？</span></p>
  <h3 data-lake-id="5ea95771" id="5ea95771"><span data-lake-id="u03eafebe" id="u03eafebe">延迟双删</span></h3>
  <p data-lake-id="u338fae0d" id="u338fae0d"><br></p>
  <p data-lake-id="u90dd42e8" id="u90dd42e8"><br></p>
  <h2 data-lake-id="LE37j" id="LE37j"><span data-lake-id="u235837fa" id="u235837fa">如何选择</span></h2>
  <p data-lake-id="ub120d7b3" id="ub120d7b3"><br></p>
  <p data-lake-id="u1ee16aa7" id="u1ee16aa7"><span data-lake-id="u63477433" id="u63477433">前面介绍了几种情况的具体问题和解决方案，那么实际工作中应该如何选择呢？</span></p>
  <p data-lake-id="uf5a54098" id="uf5a54098"><br></p>
  <p data-lake-id="uffbd239a" id="uffbd239a"><span data-lake-id="u648cedfb" id="u648cedfb">我觉得主要还是根据实际的业务情况来分析。</span></p>
  <p data-lake-id="ue0275a5e" id="ue0275a5e"><br></p>
  <p data-lake-id="ude266ba5" id="ude266ba5"><span data-lake-id="u7cdb4a73" id="u7cdb4a73">比如，如果业务量不大，并发不高的情况，可以选择</span><strong><span data-lake-id="u2fdd3d98" id="u2fdd3d98">先更新数据库</span></strong><span data-lake-id="udece3953" id="udece3953">，后删除缓存的方式，因为这种方案更加简单。</span></p>
  <p data-lake-id="ucc8968d6" id="ucc8968d6"><br></p>
  <p data-lake-id="u5bde037a" id="u5bde037a"><span data-lake-id="u0c5a2022" id="u0c5a2022">但是，如果是业务量比较大，并发度很高的话，那么建议选择</span><strong><span data-lake-id="u4793458a" id="u4793458a">先删除缓存</span></strong><span data-lake-id="ud5372984" id="ud5372984">，因为这种方式在引入延迟双删、分布式锁等机制会，会使得整个方案会更加趋近于完美，带来的并发问题更少。当然，也会更复杂。</span></p>
  <p data-lake-id="u8c5b98a7" id="u8c5b98a7"><br></p>
  <p data-lake-id="u1e942ffe" id="u1e942ffe"><span data-lake-id="ua80add43" id="ua80add43">其实，先操作数据库，后操作缓存，是一种比较典型的设计模式——</span><strong><span data-lake-id="u49b0c75a" id="u49b0c75a">Cache Aside Pattern</span></strong><span data-lake-id="ucaf4e5a2" id="ucaf4e5a2">。</span></p>
  <p data-lake-id="u4a078fa5" id="u4a078fa5"><br></p>
  <p data-lake-id="u656410bb" id="u656410bb"><span data-lake-id="uf70f9bac" id="uf70f9bac">这种模式的主要方案就是先写数据库，后删缓存，而且缓存的删除是可以在旁路异步执行的。</span></p>
  <p data-lake-id="ubde315af" id="ubde315af"><br></p>
  <p data-lake-id="ua5ff26b9" id="ua5ff26b9"><span data-lake-id="ua835caeb" id="ua835caeb">这种模式的优点就是我们说的，他可以解决"写写并发"导致的数据不一致问题，并且可以大大降低"读写并发"的问题，所以这也是Facebook比较推崇的一种模式。</span></p>
  <p data-lake-id="u6d752210" id="u6d752210"><br></p>
  <h2 data-lake-id="kGU3g" id="kGU3g"><span data-lake-id="u441fdd41" id="u441fdd41">优化方案</span></h2>
  <p data-lake-id="u7dcebe80" id="u7dcebe80"><br></p>
  <p data-lake-id="uc79e4934" id="uc79e4934"><span data-lake-id="u5831ff87" id="u5831ff87">Cache Aside Pattern 这种模式中，我们可以异步的在旁路处理缓存。其实这种方案在大厂中确实有的还蛮多的。</span></p>
  <p data-lake-id="ub0c2a1b5" id="ub0c2a1b5"><br></p>
  <p data-lake-id="uc07e2256" id="uc07e2256"><span data-lake-id="u2f4700d1" id="u2f4700d1">主要的方式就是借助数据库的binlog或者基于异步消息订阅的方式。</span></p>
  <p data-lake-id="uaf65f513" id="uaf65f513"><span data-lake-id="u3282deea" id="u3282deea">​</span><br></p>
  <p data-lake-id="uacd07fff" id="uacd07fff"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1682505057705-1fd47e44-26c6-4186-afd0-7bf323998a14.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_28%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ua2945759" id="ua2945759"><br></p>
  <p data-lake-id="ucc0c8254" id="ucc0c8254"><span data-lake-id="u9e665f5c" id="u9e665f5c">也就是说，在代码的主要逻辑中，先操作数据库就行了，然后数据库操作完，可以发一个异步消息出来。</span></p>
  <p data-lake-id="uc46daed6" id="uc46daed6"><br></p>
  <p data-lake-id="uad1f797d" id="uad1f797d"><span data-lake-id="uc737c2a1" id="uc737c2a1">然后再由一个监听者在接到消息之后，异步的把缓存中的数据删除掉。</span></p>
  <p data-lake-id="u9fe49896" id="u9fe49896"><br></p>
  <p data-lake-id="u5abe384f" id="u5abe384f"><span data-lake-id="u24d088e7" id="u24d088e7">或者干脆借助数据库的binlog，订阅到数据库变更之后，异步的清除缓存。</span></p>
  <p data-lake-id="uf7b9b501" id="uf7b9b501"><br></p>
  <p data-lake-id="u26dfeecc" id="u26dfeecc"><span data-lake-id="ucfc6d2d3" id="ucfc6d2d3">这两种方式都会有一定的延时，通常在毫秒级别，一般用于在可接受秒级延迟的业务场景中。</span></p>
  <p data-lake-id="u2d3d2361" id="u2d3d2361"><span data-lake-id="u6d502f72" id="u6d502f72">​</span><br></p>
  <blockquote data-lake-id="uf676649b" id="uf676649b">
   <p data-lake-id="u17b45c5b" id="u17b45c5b"><span data-lake-id="u5f8595c8" id="u5f8595c8">这个方案为啥可以不做延迟双删，因为我们认为基于Binlog的监听是相对可靠的，监听到binlog之后就不断的重试进行删除。而通过写代码去删除缓存是不一定可靠的，因为这个重试机制并不一定可靠。</span></p>
   <p data-lake-id="u26918900" id="u26918900"><span data-lake-id="u9b893339" id="u9b893339">当然，如果要更加完美一点，肯定还是：</span></p>
   <p data-lake-id="u5d3ae8de" id="u5d3ae8de"><span data-lake-id="u0400764b" id="u0400764b">1、先删缓存</span></p>
   <p data-lake-id="u9f3b371d" id="u9f3b371d"><span data-lake-id="u55f32dc6" id="u55f32dc6">2、再更新数据</span></p>
   <p data-lake-id="u3a671339" id="u3a671339"><span data-lake-id="ud6b8a91f" id="ud6b8a91f">3、再监听binlog删除缓存</span></p>
  </blockquote>
  <p data-lake-id="u6e1a2daf" id="u6e1a2daf"><br></p>
  <h2 data-lake-id="fFIts" id="fFIts"><span data-lake-id="ua121a46e" id="ua121a46e">缓存更新的设计模式</span></h2>
  <p data-lake-id="ue776600c" id="ue776600c"><br></p>
  <p data-lake-id="u0f9735dc" id="u0f9735dc"><span data-lake-id="ubce288a2" id="ubce288a2">前面介绍过了Cache Aside Pattern这种关于缓存操作的设计模式，那么其实还有几种其他的设计模式，也一起展开介绍一下：</span></p>
  <p data-lake-id="ueba5a1d0" id="ueba5a1d0"><br></p>
  <h3 data-lake-id="UfpvV" id="UfpvV"><span data-lake-id="u762751b2" id="u762751b2">Read/Write Through Pattern</span></h3>
  <p data-lake-id="ucb1d64ba" id="ucb1d64ba"><br></p>
  <p data-lake-id="u5305b6d2" id="u5305b6d2"><span data-lake-id="ucf835fee" id="ucf835fee">在这两种模式中，应用程序将缓存作为主要的数据源，不需要感知数据库，更新数据库和从数据库的读取的任务都交给缓存来代理。</span></p>
  <p data-lake-id="u3bb2150c" id="u3bb2150c"><br></p>
  <p data-lake-id="uba28f96b" id="uba28f96b"><strong><span data-lake-id="ud6618726" id="ud6618726">Read Through模式下</span></strong><span data-lake-id="u1c1c00e6" id="u1c1c00e6">，是由缓存配置一个读模块，它知道如何将数据库中的数据写入缓存。在数据被请求的时候，如果未命中，则将数据从数据库载入缓存。</span></p>
  <p data-lake-id="u5ea29751" id="u5ea29751"><br></p>
  <p data-lake-id="u552a0ff6" id="u552a0ff6"><strong><span data-lake-id="ua59b481d" id="ua59b481d">Write Through模式下</span></strong><span data-lake-id="uef8e1f1f" id="uef8e1f1f">，缓存配置一个写模块，它知道如何将数据写入数据库。当应用要写入数据时，缓存会先存储数据，并调用写模块将数据写入数据库。</span></p>
  <p data-lake-id="u4018f0cb" id="u4018f0cb"><br></p>
  <p data-lake-id="udf6cdee4" id="udf6cdee4"><span data-lake-id="ubabce5b0" id="ubabce5b0">也就是说，这两种模式下，不需要应用自己去操作数据库，缓存自己就把活干完了。</span></p>
  <p data-lake-id="ub77f7276" id="ub77f7276"><br></p>
  <h3 data-lake-id="Tv1GH" id="Tv1GH"><span data-lake-id="u02cc0753" id="u02cc0753">Write Behind Caching Pattern</span></h3>
  <p data-lake-id="u6a4dcb1f" id="u6a4dcb1f"><br></p>
  <p data-lake-id="ueffcc8e0" id="ueffcc8e0"><span data-lake-id="u3c93c956" id="u3c93c956">这种模式就是在更新数据的时候，只更新缓存，而不更新数据库，然后再异步的定时把缓存中的数据持久化到数据库中。</span></p>
  <p data-lake-id="ucf75aaae" id="ucf75aaae"><br></p>
  <p data-lake-id="u041960df" id="u041960df"><span data-lake-id="ue225ddc5" id="ue225ddc5">这种模式的优缺点比较明显，那就是读写速度都很快，但是会造成一定的数据丢失。</span></p>
  <p data-lake-id="ub71da11d" id="ub71da11d"><br></p>
  <p data-lake-id="ua454ee2d" id="ua454ee2d"><span data-lake-id="u4da2d354" id="u4da2d354">这种比较适合用在比如统计文章的访问量、点赞等场景中，允许数据少量丢失，但是速度要快。</span></p>
  <p data-lake-id="ufd3a19ef" id="ufd3a19ef"><br></p>
  <h2 data-lake-id="JxyGD" id="JxyGD"><span data-lake-id="ue7fc57d7" id="ue7fc57d7">没有银弹</span></h2>
  <p data-lake-id="uae2bb391" id="uae2bb391"><br></p>
  <p data-lake-id="u69c80155" id="u69c80155"><span data-lake-id="u4b945de7" id="u4b945de7">《人月神话》的作者Fred Brooks在早年有一篇很著名文章《No Silver Bullet》 ，他提到：</span></p>
  <p data-lake-id="u76642a6b" id="u76642a6b"><br></p>
  <blockquote data-lake-id="uc92442f4" id="uc92442f4">
   <p data-lake-id="u5b7f4976" id="u5b7f4976"><span data-lake-id="uc2ed1655" id="uc2ed1655">在软件开发过程里是没有万能的终杀性武器的，只有各种方法综合运用，才是解决之道。而各种声称如何如何神奇的理论或方法，都不是能杀死“软件危机”这头人狼的银弹。</span></p>
  </blockquote>
  <p data-lake-id="ue39b2be0" id="ue39b2be0"><br></p>
  <p data-lake-id="u3fa1e3b9" id="u3fa1e3b9"><span data-lake-id="u6075f3df" id="u6075f3df">也就是说，没有哪种技术手段或者方案，是放之四海皆准的。如果有的话，我们这些工程师也就没有存在的必要了。</span></p>
  <p data-lake-id="u14c9af34" id="u14c9af34"><br></p>
  <p data-lake-id="u102e1250" id="u102e1250"><span data-lake-id="u7e4942c4" id="u7e4942c4">所以，任何的技术方案，都是一个权衡的过程，要权衡的问题有很多，业务的具体情况，实现的复杂度、实现的成本，团队成员的接受度、可维护性、容易理解的程度等等。</span></p>
  <p data-lake-id="uc83e859e" id="uc83e859e"><br></p>
  <p data-lake-id="u3bdac33e" id="u3bdac33e"><span data-lake-id="u82594d74" id="u82594d74">所以，没有一个"完美"的方案，只有"适合"的方案。</span></p>
  <p data-lake-id="ue5036a9c" id="ue5036a9c"><br></p>
  <p data-lake-id="u0859966e" id="u0859966e"><span data-lake-id="ub68a8450" id="ub68a8450">但是，如何能选出一个适合的方案，这里面就需要有很多的输入来做支撑了。希望本文的内容可以为你日后的决策提供一点参考！</span></p>
 </body>
</html>